import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

public class Checker {
	String directory;
	ResultWriter out;
	int col, row;
	List<String> filenames;
	String filename;
	String test_dir;
	String author;
	String working_dir;
	String file_id;

	public Checker(ResultWriter out, String directory, String test_dir,
			List<String> filenames, int row) {
		this.directory = directory;
		this.row = row;
		this.filenames = filenames;
		this.out = out;
		this.test_dir = test_dir;
		this.working_dir = System.getProperty("user.dir");

		for (int i = 0; i < filenames.size(); i++) {
			out.addLabel(0, row++, "Name");
			out.addLabel(0, row++, "Correct name");
			if (i == 0) {
				out.addLabel(0, row++, "Subfolder");
				out.addLabel(0, row++, "# of files");
			}
			out.addLabel(0, row++, "Func.");
			out.addLabel(0, row++, "Comm. ratio");
			out.addLabel(0, row++, "Header Comm.");
			out.addLabel(0, row++, "defines");
			out.addLabel(0, row++, "Tabs");
			out.addLabel(0, row++, ">80 chars");
			out.addLabel(0, row++, "CS");
			out.addLabel(0, row++, "Global vars");
			out.addLabel(0, row++, "Realloc");
			out.addLabel(0, row++, "Malloc");
			out.addLabel(0, row++, "C Header");
			out.addLabel(0, row++, "Nesting lvl.");
			if (i == 0) {
				out.addLabel(0, row++, "GCC Warn");
				out.addLabel(0, row++, "GCC Error");
				out.addLabel(0, row++, "TC");
				out.addLabel(0, row++, "Valgrind");
			}
			row++;
		}
	}

	public void check(int col) {
		this.col = col;
		for (int i = 0; i < filenames.size(); i++) {
			filename = filenames.get(i);
			getName();
			row++;
			checkFilename();
			row++;
			if (i == 0) {
				checkSubfolder();
				row++;
				checkAdditionalFiles();
				row++;
			}
			runGCCXML();
			checkFunctions();
			row++;
			checkCommentRatio();
			row++;
			checkHeaderComments();
			row++;
			checkDefines();
			row++;
			checkTabs();
			row++;
			checkLineLength();
			row++;
			checkCS();
			row++;
			checkGlobalVars();
			row++;
			checkRealloc();
			row++;
			checkMalloc();
			row++;
			checkStandardHeader();
			row++;
			checkNestingLevel();
			row++;
			if (i == 0) {
				checkCompileWarnings();
				row++;
				checkCompileError();
				row++;
				checkTestcases();
				row++;
				checkValgrind();
				row++;
			}

			row++;
		}
	}

	private String listToCommaList(List list) {
		String res = "";
		for (Object o : list) {
			res += o.toString() + ", ";
		}
		return res.substring(0, res.length() - 2);
	}

	private void runGCCXML() {
		try {
			String[] command = new String[] { "gccxml", "-fxml=struct.xml",
					filename };
			Process child = Runtime.getRuntime().exec(command, null,
					new File(directory));
			child.waitFor();

			// get the file id
			XMLInputFactory inputFactory = XMLInputFactory.newInstance();
			InputStream in = new FileInputStream(directory + "/struct.xml");
			XMLEventReader eventReader = inputFactory.createXMLEventReader(in);
			while (eventReader.hasNext()) {
				XMLEvent event = eventReader.nextEvent();

				if (event.isStartElement()) {
					StartElement startElement = event.asStartElement();
					// If we have a item element we create a new item
					if (startElement.getName().getLocalPart() == "File") {
						if (startElement.getAttributeByName(new QName("name"))
								.getValue().equals(filename)) {
							file_id = startElement.getAttributeByName(
									new QName("id")).getValue();
							break;
						}
					}
				}
			}
			(new File(directory + "/struct.xml")).deleteOnExit();
		} catch (Exception e) {
		}
	}

	private void getName() {
		this.author = "";
		String keyword = "Authors:";
		List<Integer> author = Grep.getStringPositions(directory + "/"
				+ filename, keyword);
		// maybe it is "Author:" and not "Authors:"
		if (author.size() == 0) {
			keyword = "Author:";
			author = Grep.getStringPositions(directory + "/" + filename,
					keyword);
		}
		// or it is "Author(s):"
		if (author.size() == 0) {
			keyword = "Author(s):";
			author = Grep.getStringPositions(directory + "/" + filename,
					keyword);
		}

		if (author.size() > 0) {
			int line_nr = author.get(0);
			List<String> authors = new ArrayList<String>();
			String line, orig_line;
			do {
				line = Grep.getLineContent(directory + "/" + filename, line_nr);
				orig_line = line;
				if (line.startsWith("//----") || line.length() < 6)
					break;

				// remove author: keyword
				try {
					int start = line.indexOf(keyword) + keyword.length();
					if (!line.contains(keyword))
						start = 2;
					line = line.substring(start).trim();
				} catch (Exception e) {
				}
				// remove matrikelnumber
				try {
					// line = line.substring(0, line.length() - 7).trim();
					line = line.replaceAll("(\\d){7}", "").replace(",", "")
							.trim();
					if (!line.startsWith("Latest") && line.length() >= 3) {
						authors.add(line);
					}
				} catch (Exception e) {
					// authors.add(line.trim());
				}
				line_nr++;
			} while (orig_line.startsWith("//"));
			out.addLabel(col, row, listToCommaList(authors));
		}
	}

	private List<CFunction> getFunctions() {
		List<CFunction> items = new ArrayList<CFunction>();
		try {
			// get all function names
			XMLInputFactory inputFactory = XMLInputFactory.newInstance();
			InputStream in = new FileInputStream(directory + "/struct.xml");
			XMLEventReader eventReader = inputFactory.createXMLEventReader(in);
			CFunction item = null;
			boolean is_infile = false;

			while (eventReader.hasNext()) {
				XMLEvent event = eventReader.nextEvent();

				if (event.isStartElement()) {
					StartElement startElement = event.asStartElement();
					// If we have a item element we create a new item
					if (startElement.getName().getLocalPart() == "Function") {
						item = new CFunction();
						item.setName(startElement.getAttributeByName(
								new QName("name")).getValue());
						is_infile = startElement
								.getAttributeByName(new QName("file"))
								.getValue().equals(file_id);
						// save line number of function
						if (is_infile) {
							item.setLine(startElement.getAttributeByName(
									new QName("line")).getValue());
							item.setEndline(startElement.getAttributeByName(
									new QName("endline")).getValue());
						}
					}
				}
				// If we reach the end of an item element we add it to the list
				if (event.isEndElement()) {
					EndElement endElement = event.asEndElement();
					if (endElement.getName().getLocalPart() == "Function"
							&& is_infile) {
						items.add(item);
					}
				}

			}

		} catch (Exception e) {
		}
		return items;
	}

	private void checkFunctions() {
		List<CFunction> items = getFunctions();
		// now check the function name
		String wrong_fnames = "";
		for (CFunction func : items) {
			if (!func.getName().matches("[a-z]+[a-zA-Z0-9]*"))
				wrong_fnames += func.getName() + ", ";
		}
		if (wrong_fnames.length() > 0) {
			out.addFail(
					col,
					row,
					items.size()
							+ " ("
							+ wrong_fnames.substring(0,
									wrong_fnames.length() - 2) + ")");
		} else {
			if (items.size() > 0) {
				out.addNumber(col, row, items.size());
			} else {
				out.addFail(col, row, "parsing error");
			}
		}
	}

	private void checkHeaderComments() {
		List<CFunction> items = getFunctions();
		String no_header = "";
		String path = directory + "/" + filename;
		for (CFunction func : items) {
			int f_line = Integer.parseInt(func.getLine());
			// if there are newlines in function definition, get the first line,
			// not the last
			while (!Grep.getLineContent(path, f_line).contains(func.getName())
					&& f_line > 0)
				f_line--;
			boolean wrong = true;

			if (f_line >= 2) {
				String comm1 = Grep.getLineContent(path, f_line - 1);
				String comm2 = Grep.getLineContent(path, f_line - 2);
				if (comm1.trim().startsWith("//")
						&& comm2.trim().startsWith("//"))
					wrong = false;
			}
			if (wrong)
				no_header += func.getName() + ", ";
		}
		if (no_header.length() > 0) {
			out.addFail(col, row,
					no_header.substring(0, no_header.length() - 2));
		} else {
			out.addOk(col, row);
		}
	}

	private void checkDefines() {
		int count = 0;
		String wrong_const = "";

		try {
			FileInputStream fstream = new FileInputStream(directory + "/"
					+ filename);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String line;
			while ((line = br.readLine()) != null) {
				if (line.trim().startsWith("#define")) {
					count++;
					String constant = line.trim().split(" ")[1];
					if (!constant.toUpperCase().equals(constant)) {
						wrong_const += constant + ", ";
					}
				}
			}

			in.close();
		} catch (Exception e) {
		}

		if (wrong_const.length() > 0) {
			out.addFail(
					col,
					row,
					count
							+ " ("
							+ wrong_const.substring(0, wrong_const.length() - 2)
							+ ")");
		} else {
			out.addOk(col, row, String.valueOf(count));
		}
	}

	private void checkCommentRatio() {
		int line_count = 0;
		int comments = 0;
		int blanks = 0;
		try {
			FileInputStream fstream = new FileInputStream(directory + "/"
					+ filename);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String line;

			while ((line = br.readLine()) != null) {
				line_count++;
				if (line.trim().startsWith("//"))
					comments++;
				else if (line.trim().length() == 0)
					blanks++;
			}

			in.close();
		} catch (Exception e) {
		}

		if (comments == 0) {
			out.addFail(col, row, "0%");
		} else {
			out.addLabel(col, row, Math.round(comments * 100 / line_count)
					+ "% (" + comments + " / " + line_count + ")");
		}
	}

	private void checkGlobalVars() {
		List<CVariable> items = new ArrayList<CVariable>();
		try {
			XMLInputFactory inputFactory = XMLInputFactory.newInstance();
			InputStream in = new FileInputStream(directory + "/struct.xml");
			XMLEventReader eventReader = inputFactory.createXMLEventReader(in);
			// Read the XML document
			CVariable item = null;
			boolean is_infile = false;

			while (eventReader.hasNext()) {
				XMLEvent event = eventReader.nextEvent();

				if (event.isStartElement()) {
					StartElement startElement = event.asStartElement();
					if (startElement.getName().getLocalPart() == "Variable") {
						item = new CVariable();
						item.setName(startElement.getAttributeByName(
								new QName("name")).getValue());
						is_infile = startElement
								.getAttributeByName(new QName("file"))
								.getValue().equals(file_id);
					}
				}
				// If we reach the end of an item element we add it to the list
				if (event.isEndElement()) {
					EndElement endElement = event.asEndElement();
					if (endElement.getName().getLocalPart() == "Variable"
							&& is_infile) {
						items.add(item);
					}
				}

			}
			if (items.size() > 0) {
				out.addFail(col, row);
			} else {
				out.addOk(col, row);
			}

		} catch (Exception e) {
			out.addFail(col, row, "unknown");
		}
	}

	private void checkTestcases() {
		if (test_dir == null)
			return;

		String testfile = "tests.log";
		if (!(new File(test_dir + testfile)).exists())
			testfile = "all.log";

		System.out.println("Checking " + test_dir + testfile);

		List<Integer> fails_all = Grep.getStringPositions(test_dir + testfile,
				"[FAILED]");
		List<Integer> oks_all = Grep.getStringPositions(test_dir + testfile,
				"[  OK  ]");
		List<Integer> fails = new ArrayList<Integer>();
		List<Integer> oks = new ArrayList<Integer>();
		for (Integer f : fails_all) {
			String line = Grep.getLineContent(test_dir + testfile, f - 1);
			if (line.startsWith("----")
					|| Grep.getLineContent(test_dir + testfile, f - 2)
							.startsWith("TESTCASES"))
				fails.add(f);
		}
		for (Integer o : oks_all) {
			String line = Grep.getLineContent(test_dir + testfile, o - 1);
			if (line.startsWith("-----")
					|| Grep.getLineContent(test_dir + testfile, o - 2)
							.startsWith("TESTCASES"))
				oks.add(o);
		}

		if (fails.size() > 0) {
			out.addFail(col, row);
			out.addLink(col, row, test_dir + testfile, fails.size() + " / "
					+ (fails.size() + oks.size()));
		} else {
			out.addOk(col, row);
		}
	}

	private void checkCompileError() {
		if (test_dir == null)
			return;
		if (Grep.find(test_dir + "compile.log", "error:")) {
			out.addFail(col, row);
			out.addLink(col, row, test_dir + "compile.log", "log");
		} else {
			out.addOk(col, row);
		}
	}

	private void checkCompileWarnings() {
		if (test_dir == null)
			return;
		if (Grep.find(test_dir + "compile.log", "warning:")) {
			out.addFail(col, row);
			out.addLink(col, row, test_dir + "compile.log", "log");
		} else {
			out.addOk(col, row);
		}
	}

	private void checkValgrind() {
		if (test_dir == null)
			return;

		boolean has_warnings = false;
		try {
			FileInputStream fstream = new FileInputStream(test_dir + "all.log");
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String line;
			int line_nr = 1;
			int test_start = 0;
			boolean valgrind_section = false;

			while ((line = br.readLine()) != null) {
				if (line.contains("VALGRIND"))
					valgrind_section = true;
				// start of test
				if (valgrind_section && line.startsWith("test #"))
					test_start = line_nr;
				// end of test
				if (valgrind_section && line.startsWith("-------")) {
					if (line_nr - test_start != 8) {
						has_warnings = true;
					}
				}
				line_nr++;
			}

			in.close();
		} catch (Exception e) {
		}
		if (has_warnings) {
			out.addFail(col, row);
			out.addLink(col, row, test_dir + "all.log", "log");
		} else {
			out.addOk(col, row);
		}
	}

	private void checkCS() {

		try {
			// uncrustify
			String[] command = new String[] { "uncrustify", "-c",
					working_dir + "/check.cfg", filename };
			Process child = Runtime.getRuntime().exec(command, null,
					new File(directory));
			child.waitFor();

			// create diff
			String diff_filename = "cs_"
					+ filename.substring(0, filename.lastIndexOf('.'));
			command = new String[] {
					"/bin/sh",
					"-c",
					"diff -u " + filename + " " + filename + ".uncrustify > "
							+ diff_filename + ".diff" };

			child = Runtime.getRuntime().exec(command, null,
					new File(directory));
			child.waitFor();

			// create html report from diff
			command = new String[] { "python", working_dir + "/diff2html.py",
					"-i", directory + "/" + diff_filename + ".diff", "-o",
					directory + "/" + diff_filename + ".html" };
			child = Runtime.getRuntime().exec(command, null,
					new File(directory));
			child.waitFor();

			// if diff has 0 bytes, cs is perfect
			File diff = new File(directory + "/" + diff_filename + ".diff");
			if (diff.length() == 0) {
				out.addOk(col, row);
			} else {
				out.addFail(col, row);
				out.addLink(col, row,
						directory + "/" + diff_filename + ".html", "diff");
			}

			// delete generated files
			(new File(directory + "/cs.diff")).delete();
			(new File(directory + "/" + filename + ".uncrustify")).delete();
		} catch (Exception e) {
			System.out.println("[Error] " + e.toString());
		}
	}

	private void checkTabs() {
		List<Integer> tabs = Grep.getStringPositions(
				directory + "/" + filename, "\t");
		if (tabs.size() > 0) {
			out.addFail(col, row, listToCommaList(tabs));
		} else {
			out.addOk(col, row);
		}
	}

	private void checkAdditionalFiles() {
		out.addNumber(col, row, DirList.list(directory, null).size());
	}

	private void checkFilename() {
		if (!DirList.list(directory, null).contains(filename)) {
			out.addFail(col, row);
		} else {
			out.addOk(col, row);
			out.addLink(col, row, directory + "/" + filename, filename);
		}
	}

	private void checkSubfolder() {
		if (DirList.list(directory, DirList.dirFilter).size() > 0) {
			out.addFail(col, row);
		} else {
			out.addOk(col, row);
		}
	}

	private void checkLineLength() {
		String too_long_lines = "";
		int long_lines_cnt = 0;
		int line = 0;
		try {
			FileInputStream fstream = new FileInputStream(directory + "/"
					+ filename);
			// Get the object of DataInputStream
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			while ((strLine = br.readLine()) != null) {
				line++;
				if (strLine.length() > 80) {
					too_long_lines += String.valueOf(line) + ",";
					long_lines_cnt++;
				}
			}
			// Close the input stream
			in.close();
		} catch (Exception e) {// Catch exception if any
			System.err.println("[Error] " + e.getMessage());
		}

		if (long_lines_cnt > 0) {
			out.addFail(
					col,
					row,
					long_lines_cnt
							+ " ("
							+ too_long_lines.substring(0,
									too_long_lines.length() - 1) + ")");
		} else {
			out.addOk(col, row);
		}
	}

	private void checkRealloc() {
		int line = 0;
		String err_realloc = "";
		try {
			FileInputStream fstream = new FileInputStream(directory + "/"
					+ filename);
			// Get the object of DataInputStream
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			while ((strLine = br.readLine()) != null) {
				line++;
				if (strLine.contains("realloc")) {
					// ignore realloc in comments
					if (strLine.contains("//")
							&& strLine.indexOf("realloc") > strLine
									.indexOf("//"))
						continue;
					if (strLine.contains("/*")
							&& strLine.indexOf("realloc") > strLine
									.indexOf("/*"))
						continue;

					// get part before =
					String[] token = strLine.split("=");
					if (token.length >= 2) {
						for (int i = 1; i < token.length; i++) {
							if (token[i].contains(token[0].trim())) {
								// no temporary pointer
								err_realloc += String.valueOf(line) + ", ";
							}
						}
					} else {
						// err_realloc += String.valueOf(line) + "?, ";
					}
				}
			}
			// Close the input stream
			in.close();
		} catch (Exception e) {// Catch exception if any
			System.err.println("[Error] " + e.getMessage());
		}

		if (err_realloc.length() > 0) {
			out.addFail(col, row,
					err_realloc.substring(0, err_realloc.length() - 2));
		} else {
			out.addOk(col, row);
		}
	}

	private void checkMalloc() {
		int line = 0;
		String err_malloc = "";
		try {
			FileInputStream fstream = new FileInputStream(directory + "/"
					+ filename);
			// Get the object of DataInputStream
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			String ptr_name = "";
			while ((strLine = br.readLine()) != null) {
				line++;
				if (strLine.contains("malloc")) {
					// ignore realloc in comments
					if (strLine.contains("//")
							&& strLine.indexOf("malloc") > strLine
									.indexOf("//"))
						continue;
					if (strLine.contains("/*")
							&& strLine.indexOf("malloc") > strLine
									.indexOf("/*"))
						continue;

					// get part before =
					String[] token = strLine.split("=");
					if (token.length >= 2) {
						ptr_name = token[0].replaceAll("\\*", "")
								.replace("char", "").replace("int", "")
								.replace("long", "").trim();
					} else {
						// err_realloc += String.valueOf(line) + "?, ";
					}
				} else {
					// we had a malloc
					if (!ptr_name.equals("") && strLine.contains(ptr_name)) {
						if (!strLine.contains("NULL") && !strLine.contains("!")
								&& !strLine.contains("== 0")) {
							// usage without checking for null
							err_malloc += String.valueOf(line) + " ("
									+ ptr_name + "), ";
						} else {
							// we guess it was a correct check
						}
						ptr_name = "";
					}
				}
			}
			// Close the input stream
			in.close();
		} catch (Exception e) {// Catch exception if any
			System.err.println("[Error] " + e.getMessage());
		}

		if (err_malloc.length() > 0) {
			out.addFail(col, row,
					err_malloc.substring(0, err_malloc.length() - 2));
		} else {
			out.addOk(col, row);
		}
	}

	private void checkStandardHeader() {
		String[] allowed = { "assert.h", "ctype.h", "errno.h", "float.h",
				"limits.h", "locale.h", "math.h", "setjmp.h", "signal.h",
				"stdarg.h", "stddef.h", "stdio.h", "stdlib.h", "string.h",
				"time.h" };
		int line = 0;
		String wrong_header = "";
		
		try {
			FileInputStream fstream = new FileInputStream(directory + "/"
					+ filename);
			// Get the object of DataInputStream
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			while ((strLine = br.readLine()) != null) {
				line++;
				if (strLine.contains("#include")) {
					String header = strLine.replace("#include", "").replaceAll("\"", "").replaceAll("<", "").replaceAll(">", "").replaceAll(" ", "").trim();
					boolean is_allowed = false;
					for(int i = 0; i < allowed.length; i++) {
						if(allowed[i].equals(header.trim())) {
							is_allowed = true;
							break;
						}
					}
					if(!is_allowed) {
						wrong_header += header + " (" + line + "), ";
					}
				}
			}
			in.close();
		} catch(Exception e) {
			System.err.println("[Error] " + e.getMessage());
		}
		
		if (wrong_header.length() > 0) {
			out.addFail(col, row,
					wrong_header.substring(0, wrong_header.length() - 2));
		} else {
			out.addOk(col, row);
		}
	}
	
	
	private void checkNestingLevel() {
		final int threshold = 5;
		int line = 0;
		String deep_nesting = "";
		int depth = 0, old_depth = 0;
		
		try {
			FileInputStream fstream = new FileInputStream(directory + "/"
					+ filename);
			// Get the object of DataInputStream
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			while ((strLine = br.readLine()) != null) {
				line++;
				old_depth = depth;
				if (strLine.contains("{")) {
					depth++;
				}
				if(strLine.contains("}")) {
					depth--;
				}
				if(depth > threshold && old_depth != depth) {
					deep_nesting += String.valueOf(depth - 1) + " (" + String.valueOf(line) + "), ";
				}
			}
			in.close();
		} catch(Exception e) {
			System.err.println("[Error] " + e.getMessage());
		}
		
		if (deep_nesting.length() > 0) {
			out.addFail(col, row,
					deep_nesting.substring(0, deep_nesting.length() - 2));
		} else {
			out.addOk(col, row);
		}
	}

	private static boolean checkFor(String cmd, String output, boolean in_stream) {
		try {
			Process process = Runtime.getRuntime().exec(cmd);
			Reader r;
			if (in_stream)
				r = new InputStreamReader(process.getInputStream());
			else
				r = new InputStreamReader(process.getErrorStream());
			BufferedReader in = new BufferedReader(r);
			String line = in.readLine();
			in.close();
			if (line == null)
				return false;
			if (line.startsWith(output)) {
				System.out.println("[Info] " + line + " installed");
				return true;
			}
		} catch (Exception e) {
		}
		return false;
	}

	public static boolean checkForPython() {
		return checkFor("python -V", "Python ", false);
	}

	public static boolean checkForUncrustify() {
		return checkFor("uncrustify --version", "uncrustify ", true);
	}

	public static boolean checkForDiff() {
		return checkFor("diff --version", "diff ", true);
	}

	public static boolean checkForGCCXML() {
		return checkFor("gccxml --version", "GCC-XML", true);
	}
}
